#include <keys.h>
#include <read.h>
#include <chrono>
#include <fstream>
#include <ports.h>
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <button.h>
#include <iostream>
#include <assets.h>
#include <channel.h>
#include <tooltip.h>
#include <serialib.h>
#include <SFML/Graphics.hpp>

serialib serial;
assets resources;
channel channels [7];

std::string userInputText;
sf::Vector2i mousePos;
sf::Text plotterAxisY [1024];
sf::Text negativePlotterAxisY [1024];

uint8_t dataType;
uint8_t lineEnding;
uint8_t channelIndex;
uint8_t activeChannels;
uint8_t channelCount = 1;

bool lShift;
bool paused;
bool lControl;
bool showKeys;
bool termPrint;
bool portFound;
bool resizeInit;
bool showChannels;
bool inputFocused;
bool plotterActive;

long dataCount;
long terminalIndex;
long baudRate = 9600;

char errorOpening;
const char* port = "not specified";
float plotMultiplier = 1;
std::chrono::high_resolution_clock::time_point retryTimer;

//! Attempts to connect to the given serial port.
bool connect(const char* port)
{
    errorOpening = serial.openDevice(port, baudRate);
    return errorOpening == 1;
}

//! Attempts to find an active serial port.
void findPort(const char* ports[12])
{
    for (int i = 0; i < 12; ++i)
    {
        if (connect(ports[i]))
        {
            printf ("Successful connection to %s\n", ports[i]);
            portFound = true;
            return;
        }  
        printf ("Connection to %s failed\n", ports[i]);
    }
    printf("No active serial port found.\n");
}

//! Clears the serial plotter vertex arrays, resetting the indices.
void clearChannels()
{
    for (int i = 0; i < 7; ++i)
    {
        channels[i].plot.clear();
        channels[i].plotIndex = 0;
        channels[i].plotOrigin = 0;
        channels[i].rawValues.clear();
    }
    dataCount = 0;
}

//! writes all characters in the buffer to the serial port followed by line ending.
void sendBuffer()
{
    if (portFound && userInputText.length() > 0)
    {
        for (int i = 0; i < userInputText.length(); i++)
        {
            serial.writeChar(userInputText[i]);
        }
        
        switch(lineEnding)
        {
            case 0:
                serial.writeChar('\r');
                serial.writeChar('\n'); 
                break;
            case 1:
                serial.writeChar('\r');
                break;
            case 2:
                serial.writeChar('\n');
                break;
            default:
                break;
        }
        
        userInputText = "";
        resources.userInputDisplay = "";
        resources.sfInputText.setString("");
    }
}

//! Called whenver a key is pressed.
void handleKeyPress(sf::RenderWindow &window, sf::Event &event)
{    
    if (event.key.code == sf::Keyboard::F1)
    {
        showKeys = !showKeys;
        showChannels = false;
    }
    
    if (event.key.code == sf::Keyboard::F2)
    {
        showChannels = !showChannels;
        showKeys = false;
    }
    
    if (event.key.code == sf::Keyboard::F3)
    {
        toolTipsEnabled = !toolTipsEnabled;
    }
    
    if (event.key.code == sf::Keyboard::F4)
    {
        termPrint = !termPrint;
    }
    
    if (event.key.code == sf::Keyboard::F5)
    {
        lineEnding = lineEnding < 3 ? lineEnding + 1 : 0;
    }
    
    if (event.key.code == sf::Keyboard::F6)
    {
        dataType = dataType < 8 ? dataType + 1 : 0;
    }
    
    if (event.key.code == sf::Keyboard::F7)
    {
        channelCount = channelCount < 7 ? channelCount + 1 : 1;
    }
    
    if (event.key.code == sf::Keyboard::F8)
    {
        bigEndian = !bigEndian;
    }
    
    if (event.key.code == sf::Keyboard::F10)
    {
        plotterActive = !plotterActive;
    }
    
    if (event.key.code == sf::Keyboard::Pause)
    {
        paused = !paused;
    }
    
    if (event.key.code == sf::Keyboard::Tab)
    {
        inputFocused = !inputFocused;
    }
    
    if (event.key.code == sf::Keyboard::BackSpace)
    {
        if (resources.userInputDisplay.getSize() > 0)
        {
            resources.userInputDisplay.erase(resources.userInputDisplay.getSize() - 1, 1);
            userInputText.substr(0, userInputText.size()-1);
            resources.sfInputText.setString(resources.userInputDisplay);
        }
    }

    if (event.key.code == sf::Keyboard::Return)
    {
        sendBuffer();
    }
    
    if (!inputFocused)
    {
        for (int i = 0; i < 7; ++i)
        {
            if (event.key.code == numKeys[i])
            {
                channels[i].rawValues.clear();
                channels[i].enabled = !channels[i].enabled;
            }
        }
    }
    
    if (event.key.code == sf::Keyboard::Up)
    {
        if (plotterActive && plotBase < 10230)
        {
            plotBase += 10;
        }
        else
        {
            resources.sfMonitorText.move(0, 40);
        }
    }

    if (event.key.code == sf::Keyboard::Down)
    {
        if (plotterActive && plotBase > -10230)
        {
            plotBase -= 10;
        }
        else
        {
            resources.sfMonitorText.move(0, -40);
        }
    }
    
    if (plotterActive)
    {
        if (event.key.code == sf::Keyboard::Q)
        {
            if (plotMultiplier > 1)
            {
                plotMultiplier /= 2;
            }
        }

        if (event.key.code == sf::Keyboard::E)
        {
            if (plotMultiplier < 16384)
            {
                plotMultiplier *= 2;
            }
        }
        
        if (event.key.code == sf::Keyboard::W)
        {
            if (ySpacing < 100)
            {
                ySpacing += 10;
            }
        }

        if (event.key.code == sf::Keyboard::S)
        {
            if (ySpacing > 10)
            {
                ySpacing -= 10;
            }
        }
        
        if (event.key.code == sf::Keyboard::A)
        {
            if (xSpacing > 10)
            {
                xSpacing -= 10;
            }
        }

        if (event.key.code == sf::Keyboard::D)
        {
            if (xSpacing < 100)
            {
                xSpacing += 10;
            }
        }

        if (event.key.code == sf::Keyboard::Left)
        {
            for (int i = 0; i < 7; ++i)
            {
                channels[i].plotOrigin += 40;
                if (paused)
                {
                    channels[i].redrawPlot();
                }
            }
        }

        if (event.key.code == sf::Keyboard::Right)
        {
            for (int i = 0; i < 7; ++i)
            {
                channels[i].plotOrigin -= 40;
                if (paused)
                {
                    channels[i].redrawPlot();
                }
            }
        }
        
        if (event.key.code == sf::Keyboard::LControl)
        {
            lControl = true;
        }
        
        if (event.key.code == sf::Keyboard::LShift)
        {
            lShift = true;
        }
    }
}

//! Called whenever a key is released.
void handleKeyRelease(sf::RenderWindow &window, sf::Event &event)
{
    if (event.key.code == sf::Keyboard::LControl)
    {
        lControl = false;
    }
    
    if (event.key.code == sf::Keyboard::LShift)
    {
        lShift = false;
    }
}

//! Called whenever the mouse wheel is moved.
void handleMouseWheel(sf::RenderWindow &window, sf::Event &event)
{
    if (event.mouseWheelScroll.delta > 0)
    {
        if (lShift && plotMultiplier > 1)
        {
            plotMultiplier /= 2;
        }
        else if (lControl && ySpacing > 10)
        {
            ySpacing -= 10;
        }
        else
        {
            if (plotterActive && plotBase < 10230)
            {
                plotBase += 10;
            }
            else
            {
                resources.sfMonitorText.move(0, 40);
            }
        }
    }
    else
    {
        if (lShift && plotMultiplier < 16384)
        {
            plotMultiplier *= 2;
        }
        else if (lControl && ySpacing < 100)
        {
            ySpacing += 10;
        }
        else
        {
            if (plotterActive && plotBase > -10230)
            {
                plotBase -= 10;
            }
            else
            {
                resources.sfMonitorText.move(0, -40);
            }
        }
    }
}

//! Called whenever a mouse button is clicked.
void handleMouseClick(sf::Event &event)
{
    if(event.mouseButton.button == sf::Mouse::Left)
    {
        if (sendButton.bg.getGlobalBounds().contains(mousePos.x, mousePos.y))
        {
            sendBuffer();
        }
        
        if (lineEndButton.bg.getGlobalBounds().contains(mousePos.x, mousePos.y))
        {
            lineEnding = lineEnding < 3 ? lineEnding + 1 : 0;
        }
        
        if (typeButton.bg.getGlobalBounds().contains(mousePos.x, mousePos.y))
        {
            dataType = dataType < 8 ? dataType + 1 : 0;
        }

        if (endianButton.bg.getGlobalBounds().contains(mousePos.x, mousePos.y))
        {
            bigEndian = !bigEndian;
        }
        
        if (chanButton.bg.getGlobalBounds().contains(mousePos.x, mousePos.y))
        {
            channelCount = channelCount < 7 ? channelCount + 1 : 1;
        }
        
        if (pauseButton.bg.getGlobalBounds().contains(mousePos.x, mousePos.y))
        {
            if (paused)
            {
                paused = false;
            }
            else
            {
                paused = true;
            }
        }

        if (plotterButton.bg.getGlobalBounds().contains(mousePos.x, mousePos.y))
        {
            plotterActive = !plotterActive;
        }
        
        if (resources.entryBar.getGlobalBounds().contains(mousePos.x, mousePos.y))
        {
            if (!inputFocused)
            {
                resources.entryBar.setFillColor(sf::Color::Black);
                inputFocused = true;
            }
        }
        else
        {
            inputFocused = false;
            resources.entryBar.setFillColor(sf::Color(50, 50, 50, 255));
        }
    }
}

//! Called whenever the window is resize. Adjusts graphic elements.
void resize(sf::RenderWindow &window, sf::Event &event)
{
    if (resizeInit)
    {
        defaultPlotBase = window.getView().getSize().y - 50;
        plotBase = plotBase < defaultPlotBase ? defaultPlotBase : plotBase;
        int textEnd = resources.sfMonitorText.getPosition().y + resources.sfMonitorText.getGlobalBounds().height;
        int textDiff = (event.size.height - 40) - textEnd;
        resources.sfMonitorText.move(0, textDiff);
        window.setView(sf::View(sf::FloatRect(0, 0, event.size.width, event.size.height)));
    }
    else
    {
        resizeInit = true;
    }
}

//! Adds text to the serial output buffer.
void enterText(sf::Event &event)
{
    if (inputFocused && event.text.unicode != '\b' && resources.userInputDisplay.getSize() < 24)
    {
        resources.userInputDisplay += event.text.unicode;
        userInputText += event.text.unicode;
        resources.sfInputText.setString(resources.userInputDisplay);
    }
}

//! Handles window related events: user input, window resized, window closed.
void handleEvents(sf::RenderWindow &window)
{
    mousePos = sf::Mouse().getPosition(window);
    sf::Event event;
    while (window.pollEvent(event))
    {
        if (event.type == sf::Event::Closed)
        {
            serial.closeDevice();
            window.close();
        }

        if (event.type == sf::Event::Resized)
        {
            resize(window, event);
        }
        
        if (event.type == sf::Event::MouseButtonPressed)
        {
            handleMouseClick(event);
        }

        if (event.type == sf::Event::TextEntered)
        {
            enterText(event);
        }

        if (event.type == sf::Event::KeyPressed)
        {
            handleKeyPress(window, event);                
        }
        
        if (event.type == sf::Event::KeyReleased)
        {
            handleKeyRelease(window, event);
        }
        
        if (event.type == sf::Event::MouseWheelScrolled)
        {
            handleMouseWheel(window, event);
        }
    }
}

//! Writes data to log file.
void logText()
{
    std::ofstream file;
    file.open ("log.txt");
    file << resources.terminalDisplay.toAnsiString();
    file.close();
}

//! Handles down scaling, vertex array and text object constraints.
void checkLimits()
{
    uint8_t active = 0;
    
    for (int i = 0; i < 7; ++i)
    {
        active = channels[i].active ? active + 1 : active;
        channels[i].active = false;
    }
    
    if (activeChannels != active)
    {
        clearChannels();
        activeChannels = active;
    }

    if (terminalIndex >= 50000)
    {
        logText();
        resources.sfMonitorText.setPosition(0, 0);
        resources.terminalDisplay = "";
        terminalIndex = 0;
    }

    if (dataCount >= 100000)
    {
        clearChannels();
    }
}

//! Creates a graph using ASCII.
void plotASCII(sf::RenderWindow &window)
{
    char c;
    std::string buffer;
    
    while (serial.readChar(&c, 5))
    {
        if (c == ',' || c == '\t' || c == ' ')
        {      
            channels[channelIndex].drawPlot(window, mousePos, buffer, plotMultiplier);
            channels[channelIndex].active = true;
            channelIndex++;
            buffer = "";
            dataCount++;
        }
        else if (c == '\n')
        {      
            channels[channelIndex].drawPlot(window, mousePos, buffer, plotMultiplier);
            channels[channelIndex].active = true;

            buffer = "";
            dataCount++;
            channelIndex = 0;
            terminalIndex += 15;

            if (terminalIndex >= window.getView().getSize().y - 40)
            {
                resources.sfMonitorText.move(0, -15);
            }          
        }
        else if (channelIndex == 6)
        {
            c = '\n';
            buffer += c;
            buffer = "";
            dataCount++;
            channelIndex = 0;
            terminalIndex += 15;

            if (terminalIndex >= window.getView().getSize().y - 40)
            {
                resources.sfMonitorText.move(0, -15);
            }
        }
        else
        {
            buffer += c;
        }

        if (termPrint)
        {
            printf("%c", c);
        }

        resources.terminalDisplay += c;
    }
}

//! Creates a graph using binary values.
void plotBinary(sf::RenderWindow &window, std::string buffer)
{
    channels[channelIndex].drawPlot(window, mousePos, buffer, plotMultiplier);
    channels[channelIndex].active = true;
    dataCount++;
    terminalIndex += 15;
    channelIndex = channelIndex < channelCount - 1 ? channelIndex + 1 : 0;

    if (terminalIndex >= window.getView().getSize().y - 40)
    {
        resources.sfMonitorText.move(0, -15);
    }          

    if (termPrint)
    {
        std::cout << buffer << std::endl;
    }

    resources.terminalDisplay += buffer + "\n";
}

//! Reads data from the serial port.
void readBuffer(sf::RenderWindow &window)
{
    if (!paused)
    {
        switch(dataType)
        {
            case 0:
                plotASCII(window);
                break;
            case 1:
                plotBinary(window, readInt8(serial));
                break;
            case 2:
                plotBinary(window, readInt16(serial));
                break;
            case 3:
                plotBinary(window, readInt32(serial));
                break;
            case 4:
                plotBinary(window, readUint8(serial));
                break;
            case 5:
                plotBinary(window, readUint16(serial));
                break;
            case 6:
                plotBinary(window, readUint32(serial));
                break;
            case 7:
                plotBinary(window, readFloat(serial));
                break;
            case 8:
                plotBinary(window, readDouble(serial));
                break;
        }
        checkLimits();
    }
}

//! Draws the serial plotter.
void drawPlotter(sf::RenderWindow &window)
{
    for (int i = 0; i < 7; ++i)
    {
        if (channels[i].enabled)
        {
            window.draw(channels[i].plot);
        }
    }
    
    float x = plotterAxisY[1024].getString().getSize() * 8;
    resources.yAxisBar.setSize(sf::Vector2f(x, window.getView().getSize().y - 30));
    window.draw(resources.yAxisBar);
                
    for (int i = 0; i < 1024; ++i)
    {
        sf::VertexArray line(sf::LinesStrip, 2);
        line[0].position.x = 0;
        line[0].position.y = plotBase - (i * ySpacing);
        line[1].position.x = window.getView().getSize().x;
        line[1].position.y = plotBase - (i * ySpacing);
        line[0].color = sf::Color(240, 240, 240, 25);
        line[1].color = sf::Color(240, 240, 240, 25);
        window.draw(line);
    }
    
    for (int i = 0; i < window.getView().getSize().x; ++i)
    {
        sf::VertexArray line(sf::LinesStrip, 2);
        line[0].position.x = i * xSpacing;
        line[0].position.y = 0;
        line[1].position.x = i * xSpacing;
        line[1].position.y = window.getView().getSize().x;
        line[0].color = sf::Color(240, 240, 240, 25);
        line[1].color = sf::Color(240, 240, 240, 25);
        window.draw(line);
    }
    
    for (int i = 0; i < 1024; ++i)
    {
        plotterAxisY[i].setFont(resources.font);
        plotterAxisY[i].setCharacterSize(10);
        plotterAxisY[i].setFillColor(sf::Color::White);
        plotterAxisY[i].setPosition(sf::Vector2f(0, (plotBase - 10) - (i * ySpacing)));
        plotterAxisY[i].setString(std::to_string(static_cast<int>(i * plotMultiplier)));
        window.draw(plotterAxisY[i]);
    }
    
    for (int i = 1; i < 1024; ++i)
    {
        sf::VertexArray line(sf::LinesStrip, 2);
        line[0].position.x = 0;
        line[0].position.y = plotBase + (i * ySpacing);
        line[1].position.x = window.getView().getSize().x;
        line[1].position.y = plotBase + (i * ySpacing);
        line[0].color = sf::Color(240, 240, 240, 25);
        line[1].color = sf::Color(240, 240, 240, 25);
        window.draw(line);
    }

    for (int i = 0; i < 1024; ++i)
    {
        negativePlotterAxisY[i].setFont(resources.font);
        negativePlotterAxisY[i].setCharacterSize(10);
        negativePlotterAxisY[i].setFillColor(sf::Color::White);
        negativePlotterAxisY[i].setPosition(sf::Vector2f(0, plotBase + (i * ySpacing)));
        negativePlotterAxisY[i].setString("-" + std::to_string(static_cast<int>((i + 1) * plotMultiplier)));
        window.draw(negativePlotterAxisY[i]);
    }
}

//! Displays key bindings.
void drawKeyInfo(sf::RenderWindow &window)
{
    float width = plotterActive ? 370 : 300;
    float height = plotterActive ? 395 : 225;
    float x = (window.getView().getSize().x / 2) - (width / 2);
    float y = (window.getView().getSize().y / 2) - (height / 2);
    
    resources.guiBackground.setSize(sf::Vector2f(width, height));
    resources.guiBackground.setPosition(x, y);
    
    resources.sfGuiText.setString(guiText[plotterActive]);
    resources.sfGuiText.setPosition(x + 20, y + 10);
    
    window.draw(resources.guiBackground);
    window.draw(resources.sfGuiText);
}

//! Displays enabled state of each plotted variable.
void drawChannelInfo(sf::RenderWindow &window)
{
    std::string channelsText = "";
    for (int i = 0; i < 7; ++i)
    {
        std::string index = std::to_string(i + 1);
        std::string status = channels[i].enabled ? "enabled" : "disabled";
        channelsText += index + ": " + status + '\n';
    }
    
    float x = (window.getView().getSize().x / 2) - 57;
    float y = (window.getView().getSize().y / 2) - 65;
    
    resources.guiBackground.setSize(sf::Vector2f(115, 130));
    resources.guiBackground.setPosition(x, y);
    
    resources.sfGuiText.setString(channelsText);
    resources.sfGuiText.setPosition(x + 10, y + 10);
    
    window.draw(resources.guiBackground);
    window.draw(resources.sfGuiText);
}

//! Main loop for render window.
void drawWindow(sf::RenderWindow &window)
{
    resources.menuBar.setSize(sf::Vector2f(window.getView().getSize().x, 40));
    resources.menuBar.setPosition(0, window.getView().getSize().y - 40);
    resources.entryBar.setPosition(10, window.getView().getSize().y - 32);
    resources.sfInputText.setPosition(12, window.getView().getSize().y - 30);
    resources.sfMonitorText.setString(resources.terminalDisplay);
    
    buttonConfigParams params = { 
        window,
        resources,
        channelCount,
        lineEnding,
        dataType,
        plotterActive,
        paused
    };
    
    configureButtons(params);
    window.clear();

    if (plotterActive)
    {
        drawPlotter(window);
    }
    else
    {
        window.draw(resources.sfMonitorText);
    }

    window.draw(resources.menuBar);
    window.draw(resources.entryBar);
    window.draw(resources.sfInputText);
    
    chanButton.draw(window, mousePos);
    typeButton.draw(window, mousePos);
    sendButton.draw(window, mousePos);
    pauseButton.draw(window, mousePos);
    endianButton.draw(window, mousePos);
    lineEndButton.draw(window, mousePos);
    plotterButton.draw(window, mousePos);
    
    if (toolTipsEnabled)
    {
        updateToolTips(window, resources, channels, mousePos);
    }
    
    if (showKeys)
    {
        drawKeyInfo(window);
    }
    
    if (showChannels)
    {
        drawChannelInfo(window);
    }
    
    window.display();
}

//! Initializes graphical elements.
void initGraphics(sf::RenderWindow &window)
{   
    plotBase = window.getView().getSize().y - 50;
    defaultPlotBase = window.getView().getSize().y - 50;
    
    std::string cwd;
    char cwd_char[PATH_MAX];
    
    if (getcwd(cwd_char, sizeof(cwd_char)) != NULL)
    {
       cwd = cwd_char;
    }
    
    resources.init(window, cwd);
  
    for (int i = 0; i < 7; ++i)
    {
        channels[i].color = channelColors[i];
    }
}

//! Initializes the serial port connection.
void initSerial()
{
    if (port != "none")
    {
        portFound = connect(port);
    }

    if (!portFound)
    {
        printf("Searching for active serial port...\n");
        #if defined (_WIN32) || defined(_WIN64)
            findPort(winPorts);
        #elif defined (__linux__)
            findPort(linuxPorts);
        #elif defined(__APPLE__)
            getMacPorts();
            findPort(macPorts);
        #endif
    }
}

//! Main program.
int main(int argc, char *argv[])
{
    printf("Welcome, press F1 for help.\n");
     
    for (int i = 0; i < argc; i++)
    {
        if (strcmp(argv[i], "-b") == 0 || strcmp(argv[i], "-baud") == 0)
        {
            baudRate = atoi(argv[i + 1]);
            baudRate = baudRate == 0 ? 9600 : baudRate;
        }

        if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "-port") == 0)
        {
            port = argv[i + 1];
        }
    }

    printf("Port: %s\n", port);
    printf("Baud rate: %li\n", baudRate);
    initSerial();
    
    sf::RenderWindow window(sf::VideoMode(1280, 768), "Simple Fast Serial Plotter");
    window.setFramerateLimit(30);
    initGraphics(window);
    
    while (window.isOpen())
    {
        handleEvents(window);

        if (portFound && serial.available())
        {
            readBuffer(window);
        }
        
        if (!portFound)
        {
            std::chrono::high_resolution_clock::time_point now = std::chrono::high_resolution_clock::now();
            unsigned long elapsed = std::chrono::duration_cast<std::chrono::seconds>(now-retryTimer).count();
            if (elapsed > 3)
            {
                retryTimer = now;
                initSerial();
            }
        }

        drawWindow(window);
    }

    return 0;
}
